上一篇說到 JavaScript 原始型別與物件型別,我想今天試著來討論「傳值」與「傳址」;在其他程式語言可能可以決定要「傳值」還是「傳址」,但在 JavaScript 中我們沒辦法選擇,原始型別與物件型別會有他們個別的傳遞行為。
在 JavaScript 中,值的傳遞分為兩種:
前一篇有提到,JavaScript 中除了物件以外的所有型別,都是原始型別,也有提到分別有哪幾種,這裡就不再贅述了。原始型別也稱為「純值」,並且會以「傳值 ( Pass by value )」的方式傳遞。
直接來看看範例:
let cat = '喵'; // 定義 cat 的內容為'喵'字串
let newCat;
newCat = cat; // 將 newCat 指定等於 cat 字串
console.log(newCat); // '喵'
newCat = '呼嚕'; // 再將 newCat 的內容更動為'呼嚕'
console.log(cat); // '喵'
console.log(newCat); // 打印 newCat 出現變更後的內容'呼嚕'
這個範例看起來與我自己認知的部分沒有什麼出入,假如以這個例子來說明什麼是「 Pass by value」,我這邊可以理解的是,不能受到改變的就是「原始型別 / Pass by value ( 傳值 )」,不過為了怕自己遺忘,這邊也還是稍微解釋一下。
前一篇我們也有提到過,原始型別也有「純值」的意思,他是不可變動的,以上面的範例來說,我們可以更改 cat = '喵'
這邊的 cat
變數,但沒有辦法更改 '喵'
這個值。在電腦的空間裡面有一個大型的空間稱做「記憶體」,每一個空間都有他的位置,為了能夠讓我們取用,因此會有變數的存在,於是我們可以使用變數指向這些記憶體位置,宣告變數並賦予值,這樣的行為就是向電腦要一個記憶體空間來存值。
以 let cat = '喵'
來說,變數 cat
指向電腦中某記憶體的位置,並在這個記憶體儲存 '喵'
這個值,我們另外又宣告了一個 newCat
,雖然 cat
跟 newCat
的值是一樣的,但其實 newCat
另外在不同的記憶體位置,只是複製了 cat
的值,他們兩個仍然是各自存在於獨立的記憶體位置,因此後來我們又給 newCat
一個新的值的時候 cat
仍然不會被變動。
另外,這裡要注意的是,newCat
的值之所以變成 '呼嚕'
,是因為 newCat
改變了記憶體的指向,不是改變了 '呼嚕'
這個值,值是不能被變動的。像上面這樣的過程,我們可以稱做「傳值」。
再來一個簡易的範例,可以當作穩固概念用:
let a = 50;
let b = 50;
a === b ; // true
50
為原始型別,a
跟 b
互相比較的是彼此的值,因此這裡回傳 true
。
物件型別是以「傳址 ( Pass by reference )」的方式傳遞。一個物件型別的變數,被存在某個有地址的「位置」,而這個「記憶體位置」則存在於這個變數中,因此以「記憶體位置為參考」,並在變數間傳遞存取的行為,就稱為「傳參考呼叫」。
範例:
let arr = [1,2,3];
let newArr = arr;
console.log(arr2); // [1,2,3]
arr[0] = 2;
console.log(arr); // [2,2,3]
console.log(newArr); // [2,2,3]
來解析一下上面這一段範例,我們建立了一個陣列 arr
,這個陣列指向了一個新的記憶體位置,這時候我們再建立一個陣列 newArr
,並且讓 newArr
等於 arr
,此時 newArr
會指向 arr
的記憶體位置,因此之後不論我們怎麼修改,console.log
都會呈現 newArr === arr
這樣的結果,因為兩個記憶體指向是在同一個位置。
不過這裡還是要提到有一個例外,就是當我們直接用等號賦予新的值,就等於是建立一個新的記憶體位置,此時 arr
和 newArr
就不會指向同一個位置,因此回傳 false
。
let arr = [1,2,3];
let newArr = arr;
console.log(newArr); // [1,2,3]
arr = [4,5,6]; // 這邊賦予了新的記憶體位置
console.log(newArr); // [1,2,3]
console.log(newArr === arr); // false
結果可以看得出來,物件型別是以「記憶體位置為參考」互相傳遞,而不是以值做傳遞的過程就稱為「傳址」。
上面我們給了一個簡易的原始型別比較的範例,這邊我們可以來看看如果是物件型別的時候,是否會得到相同的結果呢?
let obj1 = { a: 1 };
let obj2 = { a: 1 };
obj1 === obj2; // false
可以看到這邊的結果跟原始物件的時候不同,這是因為每個物件都是獨立存在的實體,兩個物件的記憶體位置不同,而物件型別比較的是「記憶體位置」,不是值。
除了 Pass by value 和 Pass by reference,在查找資料的過程,還遇到了覺得其實是兩種綜合體的 Pass by sharing,剛剛我們談論到 Pass by reference 的例外,其實就是 Pass by sharing。
Pass by sharing 比較像是融合了 by value 和 by reference:
這個部分我們可以透過範例來證實:
function test(obj) {
obj = { number: 20 }; // 物件重新賦值
console.log(obj); // { number: 20 }
}
let a = { number: 10 }; // obj
test(a);
console.log(a); // { number: 10 } => 這邊沒有一起改變
來解析這個過程,宣告 function test(obj)
,接著宣告 a
變數,對 a
賦予物件 { number: 10 }
,把 a
丟進 test function
裡,等同於 obj
複製 a
,大概是長這個樣子 obj = a
,因為變數資料是物件型別,在 function
裡面又透過 obj = { number : 20 }
重新賦予值,這時候會有新的地址對應新的物件值,obj
會有新的地址,並且指向新的值。
因此 a
與 obj
的地址不同,指向的值也不同所以最後印出的 a
是 { number: 10 }
,沒有因為 obj
重新賦值而被改變。這邊有一個很大的差異是,不是透過 obj.number = 20
去改變物件的值,因為以物件型別來說這樣子寫的確會形成 Pass by reference,但今天使用的重新賦值方法為 obj = { number : 20 }
,這會改變整個 obj
的值。
最後像這樣寫法,他會有點像是 Pass by value,obj
複製了 a
,並呈現 a
與 obj
兩個記憶體位置,互相不受到影響。
以上概念其實就是混合了 Pass by value 和 Pass by reference 兩種行為,稍微解釋一下如果他們分別為 by value 或是 by reference 的情況:
obj = { number: 20 }
重新賦予值,此時會創建新的地址與值,但由於外部變數與內部變數位置不同,指向也不同,因此不會互相受到影響。obj.number = 20
改變內容,由於記憶體位置與指向都是同一個,因此會互相改變與影響。obj
重新賦值以後,以 Pass by reference 的概念來說,應該要互相影響,但實際上卻有點像是 Pass by value ,像這樣的情況其實較偏向 Pass by sharing。
寫完這篇其實自己也反覆看了幾遍,為了盡量理解每一篇看過的資料,也是反覆的測試與思考,因此這篇內容都是以自己的理解範圍下去整理,如果說明有誤還請各位看文的大大們,能不吝嗇告知,我會非常感激的!!
參考資料:
JS 原力覺醒 Day12- 傳值呼叫、傳址呼叫
重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?
[JavaScript] Javascript中的傳值 by value 與傳址 by reference
JS 變數傳遞探討:pass by value 、 pass by reference 還是 pass by sharing?